Repository navigation
Conversation
A Gree zone controller (seen: LE60-13/GH with a ME31-00/C13 WiFi module) drives the dampers of one ducted unit. It answers like a VRF gateway, but its sub-units are the ducted unit (MAC suffix 00) and up to eight zones (01 to 08), and they behave differently from VRF indoor units. Protocol layer: - GreeDevice detects the role of a sub-unit after the info fetch: it reports the name of the controller (GR-ZCntrlr...), and the MAC suffix tells the ducted unit from a zone. - The ducted unit numbers its modes 1 Cool, 2 Heat, 3 Dry, 4 Fan, 5 Auto. Before this, Fan showed as Heat and Auto raised. operation_mode and set_operation_mode convert for every unit with a zone role. - It has no Tur column. Turbo is fan speed 6, only in Cool and Heat. - A zone has a target temperature, StTem, in degrees Celsius minus 16. Only zones poll it, so other units send the same columns as before. - Discovery names the sub-units "AC unit" and "Zone 1" to "Zone 8" instead of "zone" for every one of them. Home Assistant layer: - A zone gets no climate entity. It gets a switch with the name of the zone and a Target Temperature number. - The switch is the optional feature Zone Switch, so it can be left out for a zone that must always stay open. It is only offered on zones, and zones do not show the climate options. - GreeEntityDescription.device_filter limits an entity to some kinds of device when the props alone cannot tell. Tested against a real LE60-13/GH (firmware 362001062617 V1.27) and against a new fake controller in tests/fakes/zone.py.
ZoneRole only knew the sub-units of a zone controller. It is now DeviceType and every device gets one after the info fetch: ac_unit (a standalone unit), vrf_unit (an indoor unit behind a VRF gateway), zone_controller (the ducted unit, sub-unit 00 of a zone controller) and zone. Only the two zone controller types change behaviour, so standalone and VRF units work as before. The type is in the device diagnostics. Suggested by @p-monteiro in RobHofmann#549. The controller device of a zone controller was labelled "VRF gateway". It is now "Zone controller", taken from the type of a bound sub-unit. With no bound sub-unit the registry keeps what it has. The lookup moved into a small helper, so reconcile_vrf_controllers stays simple.
Every fan mode change called set_feature_quiet and set_feature_turbo, also on a unit without those columns. The state then logged "Property Quiet is unsupported on this device" at error level on every change. Seen on the ducted unit of a zone controller, but any unit without Quiet or Turbo did it. Units that have them work as before.
RobHofmann
left a comment
There was a problem hiding this comment.
Thanks for this, and for taking in the DeviceType idea. I ran the PR in my dev instance against my standalone test unit (encryption v1). The start is clean: same 29-column probe, Device type: ac_unit, and the poll still sends the same 20 columns, without StTem. A fan change still sends Quiet and Tur as before. pytest gives 272 passed. I could not test the zone parts myself, because I have no zone controller.
One blocker and one question.
1. translation_key=UNDEFINED breaks setup when no sub-unit is bound (blocker)
In reconcile_vrf_controllers(), when the controller device already exists and _controller_info() finds no bound sub-unit, model and translation_key stay UNDEFINED and go into async_get_or_create(). That works for model, but translation_key is typed str | None, and Home Assistant does this:
if translation_key:
name = self._resolve_translated_name(config_entry, translation_key, translation_placeholders)UNDEFINED is truthy, so the name lookup falls back to the key itself and then calls .format() on it. I ran Home Assistant's own _resolve_translated_name() with UNDEFINED in my dev container:
'vrf_controller' -> 'vrf_controller'
<UndefinedType._singleton: 0> -> RAISES AttributeError 'UndefinedType' object has no attribute 'format'
The call in async_setup_entry() has no try around it. So when a local VRF gateway is offline at Home Assistant start, I expect the whole entry to fail, including standalone units in the same entry, and without a retry because it is not ConfigEntryNotReady. That also hits plain VRF users, not only zone controllers. I did not run that full scenario end to end; the crash itself is reproduced. Mypy flags the same line (helpers.py:475, arg-type), and it is the only new mypy error in the changed files.
A simple fix: only pass model, translation_key and translation_placeholders when the type is known, for example by building the kwargs and leaving those keys out when they are UNDEFINED. A test with an existing controller device and no bound sub-unit would cover it.
2. Zone Switch default in a new setup (question)
async_step_device_options() uses the options of the previous sub-unit of the same controller as defaults (_options_by_controller), and the features default is defaults.get(CONF_FEATURES, valid_features). If the AC unit is configured first, its feature list has no zone, so as far as I can read it, Zone 1 opens with Zone Switch unticked. The other way round, the AC unit could get zone as a default that is not in its options. Did you see this on your controller, or did you tick it by hand? If it is real, filtering the copied defaults against valid_features would solve both cases.
When an existing controller device had no bound sub-unit at start, for example a VRF gateway that is offline, reconcile_vrf_controllers passed UNDEFINED as translation_key. Home Assistant types it str | None and checks it with "if translation_key:", so it tried to format UNDEFINED and the whole entry failed with AttributeError, with no retry. Found in review. The model and the name arguments are now left out when the type is not known, so the registry keeps what it has. A new device still gets the VRF label. The decision moved into _controller_label. Reproduced on Home Assistant 2026.10 with a VRF device whose gateway never answers: before, the entry stayed in setup_error with every entity unavailable; after, the entry loads and the other devices work.
When the device options step starts from the options of the device of the same controller set up before, it copied all of them. So each sub-unit's form showed the name of the device before it, and the features came from a device of another kind: a zone after the AC unit started with Zone Switch off, and the AC unit after a zone got a "zone" default it cannot take. Found in review. - Only the mode and feature lists are copied now, the same lists the copy by model already used. The name stays the device's own. - The Zone Switch is never copied, because only an always open zone goes without it: a zone starts with it on, any other device without it. - The feature default is filtered against the features the device has.
cleanup_device_connection_issues deleted issues from the issue registry while it looped over registry.issues. After a device that had a "connection failed" issue was removed from the config, the next setup deleted that issue and then failed with "RuntimeError: dictionary changed size during iteration", so the entry stayed in setup_error. A second stale issue was never reached. Found while testing the controller label fix: a VRF device whose gateway never answers was removed from the YAML again, and the entry failed at the next start. A manual reload worked, because the issue was already gone.
|
Thanks for testing it on your standalone unit. Both points are fixed, plus a bug I found while testing. 1. Confirmed end to end on Home Assistant 2026.10: with a VRF device whose gateway never answers in the same entry as the zone controller, the entry failed on every start, all its entities were unavailable, and there was no retry. Fixed in 5bfa6cb. The new Mypy missed it on my side because I had only tested in my Home Assistant instance, not with a local one in the dev environment, so every Home Assistant type was Question: A test for 2. Zone Switch default (question) It is real. I had ticked it by hand. My sub-units report no
3. Bug found while testing (already on
|
| # A zone controller and its sub-units report a name that starts with this. | ||
| ZONE_CONTROLLER_NAME_PREFIX = "GR-ZCntrlr" | ||
| # The model ids of the sub-units in the sub-device list. | ||
| ZONE_CONTROLLER_MID_AC_UNIT = "5000" |
There was a problem hiding this comment.
Are we certain that all Gree Zone controllers are 5000/5001 and not only your device?
There was a problem hiding this comment.
Good question, and no, we can't be 100% sure for every model out there. Here's what I found when I dug into it:
- Those values don't come from the indoor unit at all. The WiFi module builds the sub-device list itself, and
5000,5001, thezonename andZCntrlrare all hardcoded in its firmware. I checked v1.21, v1.27 and v1.28 of the ME31-00/C13 firmware and they're the same in all three, so anything with this module should report them the same way. - The only other zone controller I could find that talks this protocol is the Braemar LE60 (Seeley), which is the same product with the same ME31-00/C13 module. The other zoning systems people put on Gree units (Airzone, AirTouch, iZone) are third party with their own protocols, so they never show up as a Gree device.
- At detection time every sub-unit reports the controller's name and model id (
GR-ZCntrlr_5000_02_..._ECand3017), not its own. So detection goes by the name, and only discovery uses the model ids.
To make this easy to extend in case it is actually different per model, 3a806af puts all of it in one table, ZONE_CONTROLLER_MODELS: one entry per model with the name prefix, the MAC suffix of the AC unit and the two model ids. The entry with the longest matching name prefix wins, so the order doesn't matter. Right now there's a general GR-ZCntrlr entry plus a GR-ZCntrlr_5000_ one for the LE60-13/GH as an example (though it is redundant, from a technical sense, and I can drop it if desired). If something like GR-ZCntrlr_3000_... turns up with other values, it just needs its own entry, and anything unknown with the same firmware still falls back to the general one. There's a test that adds a made-up model and checks both discovery and detection.
| return self._device_type | ||
|
|
||
| @property | ||
| def zone_target_temperature(self) -> int | None: |
There was a problem hiding this comment.
Do we need this? Can this logic not be wired to the existing target_temperature properties?
There was a problem hiding this comment.
Yep, good call, we can definitely refactor this to use the existing piping. I've made this change in 9f11775. Zones now go through target_temperature / set_target_temperature() like everything else, and the zone-only property is gone.
While looking into this we did some more digging and found the controller does °F as well. The wall panel has a temperature unit setting, and the WiFi module has its own columns for it: TemUnit for the unit (not TemUn) and StFahFlg as the zone's Fahrenheit bit. We tested it on the real controller. Switching TemUnit over WiFi flips the whole controller and the panel, and in °F a zone works exactly like SetTem/TemRec on a normal unit, with StTem + 16 as the Celsius part (64 °F = 2/0, 66 °F = 3/0, 67 °F = 3/1, 70 °F = 5/1). One gotcha: if you only send StTem, the controller keeps the old bit (we got 72 °F instead of 71), so both always have to go out together.
Since this is a core part of the zone controller and should work right from the start, we've included it in this PR (d7f57c4) instead of a follow-up. Your suggestion made that easy, because the zone now just reuses the existing °F conversion:
- On a zone controller,
target_temperature_unitusesTemUnit, so the existing Temperature Units select works on the AC unit and switches the whole thing. Zones don't get their own select. - The zone number follows the unit (16 to 30 °C or 61 to 86 °F), and HA shows it in your own unit. Number values are now rounded instead of cut, so HA's 69.8 °F for 21 °C goes out as 70.
- Checked through HA too: switched to °F with the select, set a zone to 21 °C, and it went out as 70 °F (
StTem5,StFahFlg1).
There was a problem hiding this comment.
You bring up a good point with TemUnit, StTem, and StFahFlg, while your tested controller and zones answer these, we don't know if they also apply to other normal ACs and VRFs.
I'm not sure if using them based on it being a zone controller or not is a good idea; maybe we could do prop support verification like for the features.
Do the controllers answer the other "old" props at all, or are they omitted from the status response?
My AC does not answer those at all. So maybe yours don't answer the "old" ones, and we can use whatever the device supports instead of the device type.
Do your devices respond to the Add0.5 prop?
There was a problem hiding this comment.
Good questions! I checked on the real controller:
- Old props: no, none of the sub-units answer
SetTem,TemRecorTemUn, they're just left out of the reply. The gateway, the AC unit and the zones all answerStTem,StFahFlgandTemUnitinstead. Add0.5: no, none of them answer it, and it isn't even in the WiFi module's column table.
So the two sets don't overlap, and your idea works. Since 2f14e87 the columns come from what a device answers, like the features:
StTem,StFahFlgandTemUnitare in the default poll, and the first poll drops them on a unit that doesn't answer them, like any other column. So your AC asks for them once, and after that sends the same columns as before. The type-basedadd_polled()is gone.- A device uses
TemUnif it answers it, andTemUnitonly (I'm assuming; untested as I don't have any other Gree hardware) if it doesn't, so every existing unit works as before. - A zone only sends its bit if it answers
StFahFlg.
There's one spot where the columns can't tell on their own: the AC unit answers StTem too (always 0), so only a zone uses StTem as its target temperature. The type is still used for that, for the zone controller's mode numbers and turbo, and for which entities a sub-unit gets. Tests cover a normal unit dropping the columns after its first poll, and TemUn winning when a unit answers both.
Rounding moved into the device as well (07f605c, see the comment on number.py).
The name prefix, the MAC suffix of the ducted unit and the two model ids in the sub-device list were separate constants, so another model with other values meant changing the code that uses them. Asked in review: are all zone controllers 5000/5001? The values come from the WiFi module, not from the units: they are fixed in its firmware (v1.21, v1.27 and v1.28 checked). Every sub-unit answers the info request with the name and model id (3017) of the controller, so detection keeps using the name and only discovery uses the model ids. ZONE_CONTROLLER_MODELS now has one entry per model. The entry with the longest matching name prefix is used, so the order does not matter and a more specific entry wins over a general one. The table has a general GR-ZCntrlr entry, and a GR-ZCntrlr_5000_ entry for the LE60-13/GH as an example of a more specific one.
Asked in review. A zone now reads and sets its target through target_temperature and set_target_temperature, like every other device, and the zone number uses them. The zone-only properties are gone. The column stays StTem, in whole degrees Celsius minus 16, clamped to 16 to 30. target_temperature is None until a zone is read, so its number shows unknown and not 0. A unit gives a value as before.
The wall panel can show Fahrenheit, and the controller has its own columns for it: TemUnit for the unit (not TemUn) and StFahFlg for the Fahrenheit bit of a zone. Checked on a LE60-13/GH by switching TemUnit over WiFi and setting zones on the panel and over WiFi: StTem + 16 and StFahFlg follow the same table as SetTem and TemRec (64 F is 2 and 0, 66 F is 3 and 0, 67 F is 3 and 1, 70 F is 5 and 1). When only StTem is sent, the controller keeps the old bit, so both are always sent. - The sub-units of a zone controller poll TemUnit, zones also StFahFlg (ZONE_CONTROLLER_PROPS). Other units send the same columns as before. - target_temperature_unit uses TemUnit on a zone controller, so the Temperature Units select works on its ducted unit and switches the whole controller, the wall panel included. Zones get no select. - The zone number follows the unit: 16 to 30 C or 61 to 86 F. Home Assistant shows it in the user's unit. - A number value is rounded, not cut, before it is sent: Home Assistant converts 21 C to 69.8 F, which must go out as 70.
Asked in review: GR-ZCntrlr is a zone controller whatever the model ids are, so why the table? Detection already went by the name and the MAC suffix only. The model ids were only used by discovery, to name the sub-units, and discovery has the name of the controller too: the scan reply of the gateway. ZONE_CONTROLLER_MODELS and the model ids are gone. Two constants are left, ZONE_CONTROLLER_NAME_PREFIX and ZONE_CONTROLLER_AC_UNIT_SUFFIX, and discovery names the sub-units from the controller name and the MAC suffix. A test gives the sub-units other model ids and still gets AC unit and Zone 1 to Zone 4.
Asked in review: StTem, StFahFlg and TemUnit were used because a device is a zone controller, but other units may answer them too. The sub-units of the zone controller answer none of SetTem, TemRec, TemUn and Add0.5, and other units do not answer the new columns. So the columns now come from what a device answers, like for the features: - StTem, StFahFlg and TemUnit are in the default poll. The first poll drops them on a unit that does not answer them, like any other column, so after it every other unit sends the same columns as before. The type-based add_polled() is gone. - A device uses TemUn when it answers it, and TemUnit only without it. - A zone sends its bit only when it answers StFahFlg. The type is still used where the columns cannot tell: the mode numbers and turbo of a zone controller, and which entities a sub-unit gets. The ducted unit answers StTem too, always with 0, so only a zone uses StTem as its target temperature.
Asked in review: 25.5 C is valid for units with half degrees, but the number entity rounded every value to a whole number. The rounding now happens in set_target_temperature, as it already did for units: Celsius to the nearest half degree, Fahrenheit to whole degrees. The number entity passes the value on as it is. The humidity number keeps its int(). A zone in Celsius takes half degrees too: StFahFlg is the half degree, like TemRec. Checked on a LE60-13/GH: StTem 2 with StFahFlg 1 was still there after a minute and after using the wall panel, which shows only whole degrees (18). So a zone now uses the same SetTem/TemRec table in both units, and its number steps by 0.5 in Celsius and 1 in Fahrenheit.
|
Thanks for the changes; it's better now. Treat the props like the sensors: The same for the temperature bit and temperature unit Then call This should give a valid Then, in the device properties, do your logic based on which props are supported. In the near future, we will gate half-degrees by a feature option that relies on the device responding to bit or Add0.5, so this keeps it ready for that. Check what we do for temperature sensors on the device, which matches the case of one property being provided by multiple fields. So that we don't need to check support in the device properties and can directly get/set the correct ones. And adjust the target temperature and temperature units accordingly to respond to the correct prop and logic. |
Context
This adds support for Gree zone controllers. It's part of porting the changes I made for my own zone controller (Gree LE60-13/GH) over to this project, and #547, #548 and #549 got things ready for it. It also picks up the
DeviceTypeidea @p-monteiro suggested in #549. Two more PRs build on this one: a per mode memory for the zone target temperatures, and the same memory for every device. Background and the full plan: https://github.com/Twintop/ha-gree-zone-controller/blob/main/upstream/README.mdWhat a zone controller is
A zone controller drives the dampers of one ducted unit. Seen: LE60-13/GH with a ME31-00/C13 WiFi module, firmware
362001062617+U-W05SAV1.27.bin(the Braemar LE60 uses the same module). It answers the scan like a VRF gateway, withsubCntabove zero, but its sub-units aren't indoor units:00is the ducted unit. It answersPow,Mod,WdSpd,AllErrandTemUnit, andStTemandStFahFlgalways as 0. ItsPowis the power of the whole system.01to08are the zones. A zone answersPow(the damper),StTemandStFahFlg(its target temperature),TemUnit, and a copy ofModandWdSpdof the ducted unit.SetTem,TemRec,TemUnorAdd0.5.GR-ZCntrlr_5000_02_..._EC), and the sub-device list calls every unitzone. The name and the00suffix of the ducted unit are fixed in the WiFi module firmware (checked v1.21, v1.27 and v1.28), not taken from the units.StTem+ 16 andStFahFlgwork likeSetTemandTemRec: in °C the bit is the half degree (the wall panel only shows whole degrees), in °F it's the same table. The unit isTemUnit(0 °C, 1 °F), notTemUn. Written to the ducted unit, the whole controller and the wall panel switch.Symptoms on 5.0-dev
zone, and the gateway device is labelled "VRF gateway".ValueErrorfromOperationMode(5).Turcolumn. Turbo is fan speed 6, so turbo is not offered, and fan speed 6 raisesValueErrorfromFanSpeed(6).StTemis not polled.Property Quiet is unsupported on this deviceat error level. This happens on any unit without Quiet.RuntimeError: dictionary changed size during iteration, becausecleanup_device_connection_issues()deletes fromregistry.issueswhile it loops over it.Change
Protocol layer:
DeviceTypefor every device, set after the info fetch:ac_unit(a standalone unit),vrf_unit(an indoor unit behind a VRF gateway),zone_controller(the ducted unit of a zone controller) andzone. A sub-unit whose name starts withZONE_CONTROLLER_NAME_PREFIXbelongs to a zone contrC_UNIT_SUFFIX` tells the ducted unit from the zones.Nothing is added to the config entry, so there is no migration. The type is in the device diagnostics.StTemre in the default poll, and the first poll drops themon a unit that doesn't answer them, like any other column, so other units send the same columns as before after it. A device usesTemUnwhen it answers it andTemUnitonly withoutere the columns can't tell: the mode numbers and turbo of the zone controller, and which entities a sub-unit gets (the ducted unit answersStTem` too, always 0).operation_modeandset_operation_mode()convertbers.feature_turbo,set_feature_turbo()andsupports_property(FEAT_TURBO_MODE)handle that.Leaving Cool or Heat drops turbo to High.
target_temperatureandset_target_temperature()like any other device, with the sameSetTem/TemRectable in °C and°F.
set_target_temperature()rounds °C to the nearee degrees. A zone write sendsStTemandStFahFlgtogether, because the controller keeps the old bit when onlyStTemcomes in.AC unitand `Zone 1oller name and the MAC suffix.Home Assistant layer:
controller's unit: 16 to 30 °C in half degrees, or 61ome Assistant shows it in your own unit.
Zones don't get their own.
zone off is refused by the controller or turns the whs only offered on zones, and zones do not show theclimate options.
GreeEntityDescription.device_filterlimits an enthen the props alone can't tell.existing device keeps its label and a new one gets thller_label()`).
not its name, and never
zone. A zone always starts the features default is filtered against what thedevice supports.int().cleanup_device_connection_issues()loops over a cen.json,icons.json,docs/protocol.md,docs/entities.md,docs/configuration.md,docs/architecture.mdandsupported-devices.mdare updated.Testing
pytest: 280 passed (255 on 5.0-dev, plus 25 new).ake controller that behaves like the real one:subDevform only, nocid, per unit columns, mode, fan and unit mirrored from the ducted unit to the zones, and an empty result abovethe column limit.
mypy.ini) and Pylint show nothing new comparedwith 5.0-dev.
StTem,StFahFlgandTemUnitof the temperature columns.StTem5,StFahFlg1). Values set on read back right.probe_device_limits()measured 29 columns. The module answers 30 with an empty result, and requests far above that (about 40 columns and more) made it reboot.